Skip to content

feat(telemetry): add simulation run context - #266

Merged
adityamehra merged 1 commit into
agent-simulation-pocfrom
codex/simulation-run-id
Oct 12, 2026
Merged

adityamehra merged 1 commit into
agent-simulation-pocfrom
codex/simulation-run-id

Conversation

@adityamehra

@adityamehra adityamehra commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Problem:

A simulation can span multiple conversations, each with its own session ID. The run ID groups those conversations’ telemetry under one simulation without changing their session identity or routing.

Telemetry:

Logger-created and native OTel spans export the run ID as splunk_ao.simulation.run_id. Supported W3C propagation carries it in baggage to downstream services; it is not a Resource attribute because it varies per run, and baggage is the propagation carrier rather than the exported span field.

Context behavior:

Use splunk_ao_context.simulation_run(...) or splunk_ao_context(simulation_run_id=...) to set the run ID. Nested splunk_ao_context(...) scopes start without the outer run ID unless explicitly given one, then restore the outer ID on exit. init() clears the active run ID. Session and experiment scoping retain their existing behavior.

@fercor-cisco fercor-cisco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A run ID that groups spans across conversations is useful, and you've wired it into both telemetry paths: spans created through the logger and native OpenTelemetry spans. Before merge, two things need fixing: a nested splunk_ao_context(...) drops the outer simulation_run() scope, and one test edit removed existing coverage. I'd also like to rename the attribute while it's still unreleased.

Please add a PR description that covers:

  • the problem this solves
  • why the run ID is a span attribute and not baggage or a Resource attribute
  • how it's meant to interact with splunk_ao_context(...), init() and session scopes

Rename the attribute. Please use splunk_ao.simulation.run_id instead of splunk_ao.simulation_run.id. A splunk_ao.simulation.* namespace leaves room for more simulation attributes later, such as scenario or persona IDs, without each one getting its own top-level prefix. Since this hasn't shipped, renaming now costs nothing. Later it would break consumers. Please update the README, ARCHITECTURE.md, CHANGELOG.md, AGENTS.md and the tests to match.

Cross-process propagation. The run ID lives only in a local context variable. It isn't in baggage and isn't carried by the session propagator wrapper. So when a simulation client calls an instrumented agent service over HTTP, the server-side spans won't have the run ID. If grouping across services is in scope, add the run ID to the propagator. If not, please state the limit in the README and ARCHITECTURE.md.

Docs. The new simulation_run_id= parameter on splunk_ao_context(...) is public, but the README and CHANGELOG.md only mention simulation_run(). Please document it, or drop it if simulation_run() is meant to be the only entry point.

_experiment_id_context.set(None)
_mode_context.set(_get_mode_or_default(None))
set_session_context(None)
_simulation_run_id_context.set(None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major. __call__ resets _simulation_run_id_context to None on entry, which breaks the README example. If a conversation wrapped in simulation_run("r") opens splunk_ao_context(session_id=...) or splunk_ao_context(project=...), no span inside that block gets the run ID.

A run ID groups spans; it isn't routing state, so it shouldn't be reset like project, Agent Stream or experiment. Please keep pushing the current value onto _simulation_run_id_stack so __exit__ restores it, but drop this reset and only override the value when simulation_run_id is passed:

Suggested change
_simulation_run_id_context.set(None)
# Simulation run scope is orthogonal to routing; inherit it unless explicitly overridden.

Please add a regression test: with simulation_run("r"): with splunk_ao_context(project="p"): ... should export a span that carries "r".

_span_stack_context.set([])
_trace_context.set(None)
set_session_context(None)
_simulation_run_id_context.set(None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major (same root cause). init() also clears an active simulation_run() scope. If someone calls init() inside a run block, the rest of that block loses the run ID. I'd remove this line. reset() (line 1369) is an explicit full reset, so clearing there seems fine.

Comment thread tests/test_splunk_ao_context.py Outdated
assert _simulation_run_id_context.get() is None


def test_simulation_run_scope_restores_the_previous_value(monkeypatch: pytest.MonkeyPatch) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major. The new test was inserted partway through test_nested_context_restoration. Everything from # First level context (line 51) onward now runs under test_simulation_run_scope_restores_the_previous_value. That has two effects:

  • The original test now only checks that values start as None, so its nesting coverage is gone.
  • The nesting block runs without the AgentStreams, Projects and Traces patches or the reset fixture. It only passes because get_logger_instance is monkeypatched.

Please move the new test after the original one and put the nesting block back. With the inheritance fix in decorator.py, the second-level assertion (_simulation_run_id_context.get() is None) should become == "run-1".

Comment thread src/splunk_ao/otel.py Outdated

if session_id:
span.set_attribute("gen_ai.conversation.id", session_id)
if simulation_run_id:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor. The two paths treat an empty run ID differently. This check is truthy, so simulation_run("") adds no attribute. SpanConverter.convert_span uses is not None, so the logger path exports "". Since simulation_run() and splunk_ao_context(simulation_run_id=...) are public inputs, I'd validate there and reject empty or non-string IDs with a ValueError. Then both paths can safely use the same check.

start_time_ns = _to_unix_ns(span.created_at)
attributes = build_span_attributes(span, session_id)
if simulation_run_id is not None:
attributes["splunk_ao.simulation_run.id"] = simulation_run_id

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. The attribute name is a raw string literal in the converter, otel.py and the tests. Please define one constant next to SPLUNK_AO_SYSTEM in converter/attribute_mapping.py, using the renamed key, so the two paths can't drift:

SPLUNK_AO_SIMULATION_RUN_ID = "splunk_ao.simulation.run_id"

@github-actions github-actions Bot locked and limited conversation to collaborators Oct 9, 2026
@adityamehra adityamehra reopened this Oct 12, 2026
@adityamehra
adityamehra changed the base branch from main to agent-simulation-poc October 12, 2026 19:42

@fercor-cisco fercor-cisco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two suggestions below. Neither blocks the merge; pick them up here or in a follow-up, whichever you prefer.

1. The local and cross-service paths disagree on whitespace in a run ID (src/splunk_ao/simulation_run_context.py:13-17)

validate_simulation_run_id() rejects a blank ID but returns the value unstripped. get_baggage_simulation_run_id() strips it, and OpenTelemetry's baggage extractor trims values anyway. So a run ID read from a file or env var with a trailing newline, such as "run-1\n", is exported as "run-1\n" on the caller's spans and as "run-1" on downstream spans. One run ends up as two groups.

Returning simulation_run_id.strip() keeps both paths consistent. A test case with surrounding whitespace would cover it, and the docstring's "return it unchanged" would need updating too.

2. The new code adds an import cycle (src/splunk_ao/simulation_run_context.py:8)

simulation_run_context → converter.attribute_mapping → converter/__init__.py → span_converter → simulation_run_context. It works today only because every entry point loads splunk_ao.converter first: logger.py imports it before this module, and session_context imports the converter first. If an import is ever reordered so simulation_run_context loads first, import splunk_ao fails with cannot import name 'validate_simulation_run_id'.

Moving SPLUNK_AO_SIMULATION_RUN_ID into simulation_run_context.py (a leaf with no splunk_ao imports) and importing it from there in attribute_mapping, span_converter, otel.py and session_context would remove the cycle.

@adityamehra
adityamehra merged commit 7605495 into agent-simulation-poc Oct 12, 2026
1 check passed
@adityamehra
adityamehra deleted the codex/simulation-run-id branch October 12, 2026 21:03
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants